/*
 * Copyright 2009 Thor Hovden
 *  
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *     http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License. 
 */
package eviva.application;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

import dev.sketch.Action;
import eviva.adapters.File;
import eviva.exceptions.Showstopper;
import eviva.exceptions.SyntaxException;
import eviva.structure.MissionControl;

/**
 * Represents an application configuration
 * 
 * @author Thor Hovden
 */
public class Appfig {

	private final char BLOCKSTART = '{';
	private final char BLOCKEND = '}';
	/** definition symbol */
	private final char COLON = ':';
	/** element separator */
	private final char COMMA = ',';

	private final String applicationGroupDef = "An Eviva application named ";
	private final String conceptsGroupDef = "Concepts";
	private final String dataGroupDef = "Data";
	private final String displaysGroupDef = "Displays";
	private final String externalsGroupDef = "Externals";
	private final String menusGroupDef = "Menus";
	private final String usersGroupDef = "Users";

	private String appname = "Noname";
	private MissionControl mission;

	private class GroupLevel {
		public int value;
	}

	private enum Group {
		APPLICATION, CONCEPTS, DATA, DISPLAYS, EXTERNALS, MENUS, USERS
	}

	private Group currentGroup;
	private final GroupLevel currentGroupLevel = new GroupLevel();
	private ParseableLine currentLine;
	private Concept currentConcept;

	private StringBuffer applicationDescription = new StringBuffer();
	private Concepts concepts = new Concepts();
	private final Action.List actionList = new Action.List();

	/**
	 * A syntax parser instance for parsing appfig (application configuration)
	 * 
	 * @param mission
	 * @param appfig String representation of application configuration
	 * @return
	 * @throws Showstopper 
	 */
	public Appfig(MissionControl mission, String appfig) throws Showstopper {
		this.mission = mission;
		parseAppfig(appfig);
		actionList.add(new Action(Action.Type.DISPLAY, concepts.display()));
		// actionList.add(new Action(Action.Type.DISPLAY, appfig));
	}

	private void parseAppfig(String appfig) throws Showstopper {
		MissionControl.Mission m = mission.set(3, "Parsing syntax");

		int lineNumber = 0;
		int errorNumber = 0;
		final int MAXERR = 5;

		BufferedReader sr = new BufferedReader(new StringReader(appfig));
		currentGroupLevel.value = 0;
		try {
			String rawLine = null;
			do {
				if (rawLine != null) {
					String trimmedLine = rawLine.trim();
					if (trimmedLine.length() > 0) {
						char c = trimmedLine.charAt(0);
						if (!isComment(c)) {
							currentLine = new ParseableLine(trimmedLine);
							try {
								mission.log(m, currentLine.toString());
								parseLine(c);
							} catch (SyntaxException e) {
								if(errorNumber<MAXERR) {
									errorNumber++;
									String err = "\n" + e.getMessage() + ", at line "
											+ lineNumber + "\n" + currentLine.getLineErrorIndicator();
									System.err.print(err);
									mission.ignore(m, err);
								} else {
									break;
								}
							}
						}
					}
				}
				rawLine = sr.readLine();
				lineNumber++;
			} while (rawLine != null);

			if(errorNumber>0) {
				String plus = (errorNumber==MAXERR ? "+" : "");
				String err = "Found "+errorNumber+plus+" errors";
				mission.abort(m, err);
				throw new Showstopper(err);
			}
			
		} catch (IOException e) {
			mission.abort(m, e);

		} finally {
			if (sr != null)
				try {
					sr.close();
				} catch (IOException e) {
					mission.ignore(m, e);
				}
		}
		mission.complete(m);
	}

	public Action.List getActions() {
		return actionList;
	}

	/**
	 * Parse one line, adding content to class variables
	 * 
	 * @param c first character of line
	 * @param currentGroupLevel number of groups parsed so far, might be updated in this method
	 * @throws SyntaxException
	 */
	private void parseLine(char c) throws SyntaxException {

		if (BLOCKSTART == c) {
			currentGroupLevel.value++;

		} else if (BLOCKEND == c) {
			currentGroupLevel.value--;

		} else if (currentGroupLevel.value == 2) {
			parseGroupLine(null);

		} else {
			String sentence1 = currentLine.parseSentence();

			if (isElementDefinition(c, currentGroupLevel)) {
				parseGroupLine(sentence1);

			} else if (isGroupDefinition(c, currentGroupLevel)) {
				currentGroup = identifyGroup(sentence1);
				parseOptionalGroupStartAtEndOfLine();

			} else {
				String msg = "Unfamiliar syntax";
				throw new SyntaxException(msg);
			}
		}
	}

	private void parseOptionalGroupStartAtEndOfLine() {
		try {
			currentLine.parseGroupStart();
			currentGroupLevel.value++;
		} catch (SyntaxException e) {
			; // ignore
		}
	}

	private void parseGroupLine(String firstSentence) throws SyntaxException {
		switch (currentGroup) {
		case APPLICATION:
			parseApplicationLine();
			break;
		case CONCEPTS:
			parseConceptLine(firstSentence);
			break;
		case DATA:
			parseDataLine(firstSentence);
			break;
		case DISPLAYS:
			parseDisplayLine(firstSentence);
			break;
		case EXTERNALS:
			parseExternalsLine(firstSentence);
			break;
		case MENUS:
			parseMenusLine(firstSentence);
			break;
		case USERS:
			parseUsersLine(firstSentence);
			break;
		}
	}

	private void parseApplicationLine() {
		applicationDescription.append(currentLine);
	}

	/**
	 * Parse a line in Concept group
	 * 
	 * @param name name of group, or null if internal group
	 * @throws SyntaxException
	 */
	private void parseConceptLine(String name) throws SyntaxException {
		if (name == null) {
			assert currentGroup != null;
			assert currentGroupLevel.value == 2;
			currentConcept.addFields(currentLine.parseSentenceList());

		} else {
			Concept c = new Concept(name);
			if (currentLine.parseColonOrGroup()) {
				currentGroupLevel.value++;
				currentConcept = c;
			} else {
				currentConcept = null;
				c.addFields(currentLine.parseSentenceList());
			}
			concepts.add(c);
		}
	}

	private void parseDataLine(String name) {
		// TODO Auto-generated method stub
	}

	private void parseDisplayLine(String name) {
		//StreamTokenizer tok;// a possible future parsing substitute
		// TODO Auto-generated method stub

	}

	private void parseExternalsLine(String name) {
		// TODO Auto-generated method stub

	}

	private void parseMenusLine(String name) {
		// TODO Auto-generated method stub

	}

	private void parseUsersLine(String name) {
		// TODO Auto-generated method stub

	}

	/**
	 * Find the part of the line, from offset to something else begins.
	 * 
	 * @param line
	 * @param fromIndex start index of line
	 * @return substring from offset to the token after begins
	 */
	private String getNextToken(String line, int fromIndex) {
		int n = line.length();

		n = getNextTokenIndex(n, line.indexOf(BLOCKSTART, fromIndex));
		n = getNextTokenIndex(n, line.indexOf(COLON, fromIndex));
		n = getNextTokenIndex(n, line.indexOf(COMMA, fromIndex));

		return line.substring(fromIndex, n).trim();
	}

	private int getNextTokenIndex(int currentIndex, int candidateIndex) {
		if (candidateIndex != -1)
			currentIndex = (candidateIndex < currentIndex ? candidateIndex : currentIndex);
		return currentIndex;
	}

	private Group identifyGroup(String groupDef) throws SyntaxException {
		Group g = null;
		if (groupDef.startsWith(applicationGroupDef)) {
			appname = getNextToken(groupDef, applicationGroupDef.length());
			g = Group.APPLICATION;
		} else if (conceptsGroupDef.equalsIgnoreCase(groupDef)) {
			g = Group.CONCEPTS;
		} else if (dataGroupDef.equalsIgnoreCase(groupDef)) {
			g = Group.DATA;
		} else if (displaysGroupDef.equalsIgnoreCase(groupDef)) {
			g = Group.DISPLAYS;
		} else if (externalsGroupDef.equalsIgnoreCase(groupDef)) {
			g = Group.EXTERNALS;
		} else if (menusGroupDef.equalsIgnoreCase(groupDef)) {
			g = Group.MENUS;
		} else if (usersGroupDef.equalsIgnoreCase(groupDef)) {
			g = Group.USERS;
		} else {
			throw new SyntaxException("Unreckognized group definition [" + groupDef + "]");
		}
		return g;
	}

	private boolean isGroupDefinition(char c, GroupLevel groupLevel) {
		return groupLevel.value == 0 && Character.isLetter(c);
	}

	private boolean isElementDefinition(char c, GroupLevel groupLevel) {
		return Character.isLetter(c) && groupLevel.value > 0;
	}

	private boolean isComment(char c) {
		return !(Character.isLetter(c) || BLOCKSTART == c || BLOCKEND == c);
	}

	/**
	 * Process appfig file
	 * 
	 * @param mission
	 * @param filename Eviva appfig file name, like tasklist.ev
	 * @throws Showstopper
	 */
	public static Appfig parseFile(MissionControl mission, String filename) throws Showstopper {
		MissionControl.Mission m = mission.set(3, "Parse file");
		File file = new File(mission, filename);
		String fileContent = file.read();
		Appfig appfig = new Appfig(mission, fileContent);
		mission.complete(m);
		return appfig;
	}

	public String getAppname() {
		return appname;
	}

}
